Skip to content

feat: 에러 핸들링 및 공통 UI 처리#118

Merged
seoyoon513 merged 12 commits intodevelopfrom
BOOK-233-feature/#117
Aug 8, 2025
Merged

feat: 에러 핸들링 및 공통 UI 처리#118
seoyoon513 merged 12 commits intodevelopfrom
BOOK-233-feature/#117

Conversation

@seoyoon513
Copy link
Copy Markdown
Contributor

@seoyoon513 seoyoon513 commented Aug 8, 2025

🔗 관련 이슈

📙 작업 설명

  • core:ui 에 ReedErrorUi로 화면별 에러 처리
  • EventBus를 활용하여 다이얼로그 띄우는 에러 전역 처리
  • 도서 상세 화면에서 초기 로드를 async로 구현

🧪 테스트 내역

  • 주요 기능 정상 동작 확인
  • 브라우저/기기에서 동작 확인
  • 엣지 케이스 테스트 완료
  • 기존 기능 영향 없음

📸 스크린샷 또는 시연 영상

✅ 전체 화면 에러 (화면에 데이터를 불러와 렌더링 해야하는 경우)

  • 네트워크 에러 문구 : 네트워크 연결이 불안정합니다. 인터넷 연결을 확인해주세요
  • 그 외 서버 에러 문구 : 알 수 없는 문제가 발생했어요. 다시 시도해주세요
내서재 도서 검색 / 내서재 검색
Reed_홈_에러 Reed_내서재_에러 Reed_검색_에러
도서 상세 기록 상세
Reed_도서상세_에러 Reed_기록상세_에러

✅ 에러 다이얼로그 (이미 화면을 보고 있는 상태에서 요청이 있는 경우)

  • 네트워크 에러 문구 : 네트워크 연결이 불안정합니다. 인터넷 연결을 확인해주세요
  • 그 외 서버 에러 문구 : 상황별 상이함. ErrorScope로 Dialog message 분기(아래 코드 참고)
  • 현재 전역 다이얼로그가 적용된 부분 -> 도서등록, 기록 저장
도서 등록 네트워크 에러 기록 저장 서버 에러
Reed_도서등록_에러 Reed_기록저장실패_서버에러
  • 상황별 message를 대응하기 위해 ErrorScope 설정
  • RootView인 MainActivity에서 전역 에러 이벤트 collect -> 다이얼로그 띄우기
private fun buildDialog(
    scope: ErrorScope,
    exception: Throwable,
    action: () -> Unit,
): ErrorDialogSpec {
    val message = when {
        exception.isNetworkError() -> {
            "네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요"
        }
        exception is HttpException -> {
            when (scope) {
                ErrorScope.GLOBAL -> {
                    "알 수 없는 문제가 발생했어요.\n다시 시도해주세요"
                }

                ErrorScope.LOGIN -> {
                    "예기치 않은 오류가 발생했습니다.\n다시 로그인 해주세요."
                }

                ErrorScope.BOOK_REGISTER -> {
                    "도서 등록 중 오류가 발생했어요.\n다시 시도해주세요"
                }

                ErrorScope.RECORD_REGISTER -> {
                    "기록 저장에 실패했어요.\n다시 시도해주세요"
                }
            }
        }
        else -> {
            "알 수 없는 문제가 발생했어요.\n다시 시도해주세요"
        }
    }

    return ErrorDialogSpec(message = message, buttonLabel ="확인" , action = action)
}

💬 추가 설명 or 리뷰 포인트

📌 For PM

현재 에러 UI를 다이얼로그로만 보여주는 것에 대해,

1. 화면 렌더링 단계의 에러

  • 화면에 데이터를 렌더링 하는 시점에서 오류가 발생하면, 전체 화면을 덮는 에러 UI를 노출하는게 적절하다고 생각합니다.
  • 이유:
    • 다이얼로그에서 다시 시도 버튼만 제공하면 해당 문제가 해결되기 전까지는 다른 화면 탐색이 불가능합니다
    • 확인 버튼을 제공하여 다이얼로그를 닫을 시에도 에러 대응이 되지 않은 화면이 사용자에게 그대로 노출됩니다 (버그로 인식)
    • 현재 당근마켓, 오늘의집, 쿠팡 모두 에러 화면으로 덮는 전략을 사용하고 있습니다

2. 화면 진입 이후 추가 데이터 요청 시 발생하는 에러 (로그인, 도서 등록, 기록 등록 등)

  • 예: 로그인, 도서 등록, 기록 저장 등
  • 이 경우 다이얼로그를 띄우는 것이 적절하나, 꼭 다이얼로그를 띄워야 하는지는 검토가 필요합니다.
  • 이유:
    • 다이얼로그는 사용자의 주의를 강하게 끌고 액션을 강제하는 UI입니다.
    • 단순 알림이나 확인만 필요한 경우, 스낵바나 토스트로 안내하는 것이 자연스러울 수 있습니다

3. 에러 메세지 가이드 개선

  • 현재 네트워크 오류를 제외한 문구들이 모호합니다.
    • 예) "기록 저장에 실패했어요. 다시 시도해주세요" -> 사용자가 왜 실패했는지 알 수 없음. 원인에 대한 간략한 설명 필요.

4. 2회 초과 요청 다이얼로그 분기 불필요

  • 1회 실패든 2회 초과든, 근본적인 해결책은 "네트워크/서버 상태 확인 후 재시도"로 동일합니다
  • 별도 UI로 관리하면 문구, 디자인, 로직 분기 등 유지보수 복잡성만 증가하고, "2회 초과"를 안내하는 것은 UX적으로 추가 가치가 적다고 생각합니다

📌 For 지훈

  • 현재 에러 관련 토스트로 처리한 부분은 굳이 제거하지 않았습니다. 기능 QA이후 결정이 되면 그때 다듬어요!
  • 401 세션 만료까지는 제가 처리를 못했습니다... 이것 또한 QA 이후로 다듬겠습니다😢
  • 네트워크 없는 환경에서 Splash 진입 시 바로 로그인 화면으로 이동하는 이슈 있습니다. Splash에서 재시도 다이얼로그를 띄우던지 해야할 것 같네요
  • 로그인,로그아웃,약관동의,회원탈퇴 -> 모두 다이얼로그로 일괄 처리해야하나 일단 보류하겠습니다
  • 특히 로그인의 경우 카카오톡로그인, Reed회원가입/로그인 으로 구분되어 있어 이 부분 에러 처리도 어떻게 해야할지 고민이 필요합니다

Summary by CodeRabbit

  • 신규 기능

    • 앱 전역에서 발생하는 오류를 안내하는 다이얼로그가 추가되어 사용자에게 명확한 피드백을 제공합니다.
    • 에러 UI 컴포넌트가 도입되어 네트워크 및 서버 오류 시 재시도 버튼과 함께 친숙한 메시지를 표시합니다.
  • 버그 수정

    • 에러 상태에 예외 객체를 포함하도록 개선하여, 보다 정확하고 상세한 오류 정보를 제공합니다.
  • UI/UX 개선

    • 로딩, 성공, 에러 상태를 명확히 구분하여 사용자 경험을 향상시켰습니다.
    • 각 화면에서 오류 발생 시 일관된 에러 UI와 재시도 기능을 지원합니다.
  • 문서 및 리소스

    • 네트워크 및 서버 오류 관련 안내 문구가 추가되고, 사용하지 않는 문자열 리소스가 정리되었습니다.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 8, 2025

Walkthrough

에러 핸들링 및 UI 처리를 대대적으로 개선하는 작업이 이루어졌습니다. 에러 상태를 단순 메시지에서 예외 객체로 일원화하고, 에러 UI 컴포넌트(ReedErrorUi)를 도입하여 여러 화면에서 일관된 에러 표시 및 재시도 기능을 제공합니다. 글로벌 에러 다이얼로그 처리, 에러 스코프 구분, 에러 이벤트 헬퍼 등도 추가되었습니다.

Changes

Cohort / File(s) Change Summary
에러 다이얼로그/이벤트/스코프 도입
core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt, core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt, core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt, core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt
에러 다이얼로그 명세, 에러 스코프 enum, 에러 이벤트 헬퍼, 에러 다이얼로그 전송 함수 및 네트워크 에러 판별 함수 공개 등 에러 처리 인프라 신규 도입
UI 모듈 에러 UI/리소스/빌드
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt, core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt, core/ui/src/main/res/values/strings.xml, core/ui/build.gradle.kts
에러 전용 UI 컴포넌트(ReedErrorUi) 추가, 다이얼로그 제목 파라미터 nullable로 변경, 에러 관련 문자열 리소스 추가, 공통 모듈 의존성 추가
상세/기록/홈/검색/라이브러리 프리젠터 및 UI
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt, feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt, feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt, feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt, feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt, feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt, feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt, feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt
에러 상태를 예외 객체로 통일, 에러 UI를 ReedErrorUi로 대체, 재시도 이벤트 추가, 에러 처리 및 페이징 로직 개선 등
글로벌 에러 다이얼로그 적용
feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
글로벌 에러 이벤트 수신 및 다이얼로그 표시 로직 추가
기타 컴포넌트/리소스 변경
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt, feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt, feature/library/src/main/res/values/strings.xml, feature/search/src/main/res/values/strings.xml
감정 이미지 함수 공개, 감정 이미지 표시 방식 변경, 불필요한 문자열 리소스 삭제 등

Sequence Diagram(s)

sequenceDiagram
    participant Presenter
    participant ErrorEventHelper
    participant MainActivity
    participant ReedDialog

    Presenter->>ErrorEventHelper: postErrorDialog(scope, exception, action)
    ErrorEventHelper->>MainActivity: emit ErrorEvent.ShowDialog(spec)
    MainActivity->>ReedDialog: show dialog with spec
    ReedDialog->>MainActivity: on confirm, invoke action
Loading
sequenceDiagram
    participant Presenter
    participant UI

    Presenter->>UI: emit UiState.Error(exception)
    UI->>ReedErrorUi: show error UI with exception, onRetryClick
    ReedErrorUi->>Presenter: onRetryClick event
    Presenter->>UI: retry logic, update state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Assessment against linked issues

Objective Addressed Explanation
에러 핸들링 및 UI 처리 (#117)

Assessment against linked issues: Out-of-scope changes

(해당 변경사항 없음)

Suggested reviewers

  • easyhooon

Poem

에러가 와도 걱정 마,
토끼가 만든 에러 UI가 반짝!
다이얼로그 띄우고, 재시도도 척척,
예외를 품은 코드가 든든하게 지켜줄 거야.
🐇✨

"이젠 에러도 귀엽게, 북잇에서 해결해요!"

Note

🔌 MCP (Model Context Protocol) integration is now available in Early Access!

Pro users can now connect to remote MCP servers under the Integrations page to get reviews and chat conversations that understand additional development context.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch BOOK-233-feature/#117

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@easyhooon
Copy link
Copy Markdown
Contributor

easyhooon commented Aug 8, 2025

도서 등록 네트워크 에러 기록 저장 서버 에러
Reed_도서등록_에러 Reed_기록저장실패_서버에러

현재 에러 관련 토스트로 처리한 부분은 굳이 제거하지 않았습니다. 기능 QA이후 결정이 되면 그때 다듬어요!

여기 다이얼로그 있으면 굳이 토스트 안보여줘도될것같긴하네여

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🔭 Outside diff range comments (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (2)

92-100: 감정 리소스 매핑 함수 public 전환: 안정성 주석 및 타입 힌트 추가 권장

  • displayName(표시 문자열)에 의존하면 로케일/카피 변경 시 매핑이 깨질 수 있습니다. 가능하면 안정적인 코드/enum(예: 서버 코드나 core:model의 EmotionModel 등)에 기반한 매핑으로 전환을 권장합니다.
  • 리소스 ID 반환에는 @DrawableRes 애노테이션을 부여해 호출 측 타입 안정성을 높여주세요.

예시:

+import androidx.annotation.DrawableRes
- fun getEmotionImageResourceByDisplayName(displayName: String): Int {
+ @DrawableRes
+ fun getEmotionImageResourceByDisplayName(displayName: String): Int {
   return when (displayName) {
     "따뜻함" -> R.drawable.img_warm
     "즐거움" -> R.drawable.img_joy
     "슬픔" -> R.drawable.img_sad
     "깨달음" -> R.drawable.img_insight
     else -> R.drawable.img_warm
   }
 }

추가 제안(선택): displayName 기반 함수는 유지하되, 내부적으로 안정 키→리소스 매핑을 우선하고, displayName→안정 키 변환 계층을 분리해두면 i18n 변화에 견고해집니다.


62-69: 빈 emotionTags 인덱싱으로 인한 충돌 가능성 (IndexOutOfBoundsException) 방지 필요

Preview에서 emotionTags = persistentListOf()로 비어있는 리스트를 넣고 있고, 런타임에서도 비어있을 수 있습니다. 현재 emotionTags[0] 접근은 즉시 크래시를 유발합니다.

안전 접근으로 변경 제안:

- Image(
-     painter = painterResource(getEmotionImageResourceByDisplayName(emotionTags[0])),
-     contentDescription = "Emotion Graphic",
-     modifier = Modifier
-         .size(40.dp)
-         .clip(CircleShape),
- )
+ val primaryEmotion = emotionTags.firstOrNull()
+ if (primaryEmotion != null) {
+   Image(
+     painter = painterResource(getEmotionImageResourceByDisplayName(primaryEmotion)),
+     contentDescription = stringResource(R.string.cd_emotion_graphic),
+     modifier = Modifier
+       .size(40.dp)
+       .clip(CircleShape),
+   )
+ }

태그 텍스트도 동일하게 보호:

- Text(
-   text = "#${emotionTags[0]}",
-   ...
- )
+ if (primaryEmotion != null) {
+   Text(
+     text = "#$primaryEmotion",
+     ...
+   )
+ }

참고: R.string.cd_emotion_graphic는 strings.xml에 추가 필요합니다(아래 i18n 코멘트 참고).

Also applies to: 70-76

🧹 Nitpick comments (18)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt (1)

3-5: KDoc 보강 제안 (의도와 사용처 명시)

간단한 주석만으로도 가독성과 일관성이 높아집니다. 아래처럼 추가를 고려해주세요.

 package com.ninecraft.booket.core.common.constants

+/**
+ * 에러가 발생/표시되는 영역을 구분합니다.
+ * - GLOBAL: 앱 전역(메인 등)에서의 에러
+ * - LOGIN: 로그인/세션 관련 에러
+ * - BOOK_REGISTER: 책 등록 플로우 관련 에러
+ * - RECORD_REGISTER: 기록 등록 플로우 관련 에러
+ */
 enum class ErrorScope {
-    GLOBAL, LOGIN, BOOK_REGISTER, RECORD_REGISTER
+    GLOBAL, LOGIN, BOOK_REGISTER, RECORD_REGISTER
 }
core/ui/src/main/res/values/strings.xml (1)

5-6: 문구/띄어쓰기 미세 개선 제안 (선택사항)

사내 문체 가이드에 따라 아래 형태도 고려해 보세요.

-<string name="network_error_message">네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해주세요</string>
-<string name="server_error_message">알 수 없는 문제가 발생했어요.\n다시 시도해주세요</string>
+<string name="network_error_message">네트워크 연결이 불안정합니다.\n인터넷 연결을 확인해 주세요</string>
+<string name="server_error_message">알 수 없는 문제가 발생했어요.\n잠시 후 다시 시도해 주세요</string>

또한, 다이얼로그/풀스크린 UI에서 줄바꿈 적용이 일관적으로 보이는지 실제 기기에서 한 번만 더 확인해 주세요.

feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt (1)

12-17: @immutable 주석 추가 고려

Compose 재구성 비용 감소 및 분석기 힌트를 위해 UiState에도 @immutable 적용을 권장합니다.

 sealed interface UiState {
+    @Immutable
+    // 또는 sealed interface 자체에 @Immutable 부여
     data object Idle : UiState
     data object Loading : UiState
     data object Success : UiState
     data class Error(val exception: Throwable) : UiState
 }

참고: sealed interface 자체에 @immutable를 붙이는 방식도 가능합니다(팀 컨벤션에 맞춰 선택).

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt (1)

13-18: @immutable 적용으로 Compose 친화성 강화

Home과 동일하게 UiState에도 @immutable 부여를 권장합니다.

 sealed interface UiState {
+    @Immutable
     data object Idle : UiState
     data object Loading : UiState
     data object Success : UiState
     data class Error(val exception: Throwable) : UiState
 }
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1)

64-64: contentDescription 하드코딩(i18n) → stringResource 사용 권장

접근성 문자열은 현지화 리소스로 관리해야 합니다.

예시:

- contentDescription = "Emotion Graphic",
+ contentDescription = stringResource(R.string.cd_emotion_graphic),

필요 시 feature/detail 모듈의 res/values/strings.xmlcd_emotion_graphic 키를 추가해 주세요. 매개값(예: 감정명)을 포함시키려면 포맷 문자열(%1$s)로 정의 후 stringResource(R.string.cd_emotion_graphic, primaryEmotion) 형태로 사용 가능합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (1)

24-24: 교차 컴포넌트 의존 최소화를 위한 유틸 분리 제안

getEmotionImageResourceByDisplayName가 ‘book/component/RecordItem.kt’에 위치한 채 ‘record/component’에서 참조되고 있습니다. detail 기능 전반에서 사용하는 매핑이므로 feature/detail/.../common/EmotionResources.kt 같은 파일로 추출해 두면 모듈(패키지) 간 결합이 낮아지고 재사용/테스트가 수월해집니다.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (1)

90-97: 에러 표현 형식 통일 제안

UiState에는 Throwable을 그대로 넘기는데, 같은 지점의 footerState는 문자열만 넘기고 있습니다.
두 컴포넌트가 다른 타입을 요구하더라도, 한쪽만 예외 객체·한쪽만 메시지로 관리하면 추후 파싱/로깅 로직이 이원화됩니다.
가능하다면 FooterState.Error도 예외 객체를 받아 내부에서 메시지를 파생하도록 통일해보세요.

core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt (1)

3-7: action 기본값 추가 제안

대부분의 에러 다이얼로그는 별도 액션 없이 닫기만 합니다. action: () -> Unit = {} 로 기본값을 주면 호출부에서 람다 생략이 가능해 코드가 간결해집니다.

core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt (1)

66-73: 불필요한 변수 참조

title?.let { … Text(text = title, …) } 구문에서 람다 내부는 it 를 바로 쓰는 편이 명확합니다.

- title?.let {
-     Text(
-         text = title,
+ title?.let {
+     Text(
+         text = it,
feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt (2)

59-69: 전역 에러 이벤트 연속 수신 시 덮어쓰기 가능성

다이얼로그가 떠있는 동안 새 이벤트가 오면 기존 스펙을 덮어씁니다. UX 상 혼란이 있을 수 있으니 큐잉 또는 표시 중엔 드롭 등 정책을 정해 주세요. 간단히 드롭하려면 아래처럼 가드 가능합니다.

 LaunchedEffect(Unit) {
     ErrorEventHelper.errorEvent.collect { event ->
-        when (event) {
+        if (dialogSpec.value != null) return@collect
+        when (event) {
             is ErrorEvent.ShowDialog -> {
                 dialogSpec.value = event.spec
             }
         }
     }
 }

필요 시 SnapshotStateList로 큐를 두고, confirm/dismiss 시 다음 항목을 표시하는 방식도 고려해 보세요.


57-57: 설정 변경 시 다이얼로그 유지가 필요하다면 saveable 고려

dialogSpec는 remember 상태라 회전 등 구성 변경 시 사라집니다. 유지가 필요하면 Saver를 제공해 rememberSaveable로 전환을 검토해 주세요(단, ErrorDialogSpec이 직렬화 가능해야 합니다).

core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (2)

23-31: 재사용성 향상을 위한 modifier 파라미터 추가 제안

항상 fillMaxSize로 고정되어 있어 섹션 단위(부분 영역) 에러 UI 재사용이 어렵습니다. modifier를 받아 확장 가능하게 해주세요.

-@Composable
-fun ReedErrorUi(
-    exception: Throwable,
-    onRetryClick: () -> Unit,
-) {
+@Composable
+fun ReedErrorUi(
+    exception: Throwable,
+    onRetryClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
-    val message = if (exception.isNetworkError()) stringResource(R.string.network_error_message) else stringResource(R.string.server_error_message)
-    Box(
-        modifier = Modifier.fillMaxSize(),
+    val message =
+        if (exception.isNetworkError())
+            stringResource(R.string.network_error_message)
+        else
+            stringResource(R.string.server_error_message)
+    Box(
+        modifier = modifier.fillMaxSize(),
         contentAlignment = Alignment.Center,
     ) {

27-27: 가독성: 메시지 분기 한 줄 표기 → 여러 줄로 정리(선택)

조건식이 길어 가독성이 떨어집니다. when/여러 줄 if로 정리하면 읽기 쉬워집니다.

-    val message = if (exception.isNetworkError()) stringResource(R.string.network_error_message) else stringResource(R.string.server_error_message)
+    val message = if (exception.isNetworkError()) {
+        stringResource(R.string.network_error_message)
+    } else {
+        stringResource(R.string.server_error_message)
+    }
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (1)

65-93: 하드코딩된 오류 메시지를 문자열 리소스로 분리 권장

buildDialog 내부의 한국어 메시지가 코드에 직접 하드코딩돼 있습니다.
향후 다국어 지원 및 유지보수를 위해 core/common/res/values/strings.xml 등의 리소스로 옮겨 주세요.
이 변경은 UI 모듈의 문자열 관리 방식과도 일관성을 맞추는 데 도움이 됩니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (4)

136-145: Idle 상태에서 빈 화면 대신 Loading과 통합해 UX 플리커를 줄여주세요.

초기 진입 시 Idle → 즉시 Loading 전환되는 동안 빈 화면이 잠깐 보일 수 있습니다. Idle과 Loading을 묶어 동일한 로딩 UI를 보여주는 편이 자연스럽습니다.

-    when (state.uiState) {
-        is UiState.Idle -> {}
-        is UiState.Loading -> {
+    when (state.uiState) {
+        is UiState.Idle,
+        is UiState.Loading -> {
             Box(
                 modifier = Modifier.fillMaxSize(),
                 contentAlignment = Alignment.Center,
             ) {
                 CircularProgressIndicator(color = ReedTheme.colors.contentBrand)
             }
         }

187-192: 접근성: 장식 아이콘은 contentDescription을 null로 설정

해당 아이콘은 드롭다운 표시용 장식 요소로 보입니다. 버튼 텍스트가 의미를 전달하므로 스크린리더에 노출하지 않도록 contentDescription = null을 권장합니다.

-                                    Icon(
-                                        imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_down),
-                                        contentDescription = "Dropdown Icon",
+                                    Icon(
+                                        imageVector = ImageVector.vectorResource(designR.drawable.ic_chevron_down),
+                                        contentDescription = null,
                                         modifier = Modifier.size(22.dp),
                                         tint = ReedTheme.colors.contentPrimary,
                                     )

253-273: Lazy 리스트는 items(list) 변형을 사용하면 더 간결하고 안전합니다

인덱스 접근 기반 items(count) 대신 items(items, key)가 가독성/안정성(리스트 변경 시 OOB 방지)에 유리합니다.

-                } else {
-                    items(
-                        count = state.readingRecords.size,
-                        key = { index -> state.readingRecords[index].id },
-                    ) { index ->
-                        val record = state.readingRecords[index]
+                } else {
+                    items(
+                        items = state.readingRecords,
+                        key = { record -> record.id },
+                    ) { record ->
                         RecordItem(
                             quote = record.quote,
                             emotionTags = record.emotionTags.toImmutableList(),
                             pageNumber = record.pageNumber,
                             createdAt = record.createdAt.toFormattedDate(),
                             modifier = Modifier
                                 .padding(
                                     start = ReedTheme.spacing.spacing5,
                                     end = ReedTheme.spacing.spacing5,
                                     bottom = ReedTheme.spacing.spacing3,
                                 )
                                 .clickable {
-                                    state.eventSink(BookDetailUiEvent.OnRecordItemClick(record.id))
+                                    state.eventSink(BookDetailUiEvent.OnRecordItemClick(record.id))
                                 },
                         )
                     }

301-303: Preview 데이터 충분성 검증 요청

UiState.Success만 지정되어 있는데, BookDetailUiState의 다른 필드(예: bookDetail, readingRecords, seedsStats 등)가 non-null/기본값을 요구한다면 프리뷰가 깨질 수 있습니다. 팀에서 더미 데이터를 선호하므로, preview 전용 팩토리/스텁(state.preview())를 제공하거나 프리뷰 내에서 최소 더미를 세팅하는 것을 권장합니다.

원하시면 preview용 스텁 생성/적용 패치를 작성해 드리겠습니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecbfa0b and 7136609.

📒 Files selected for processing (32)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorDialogSpec.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (3 hunks)
  • core/ui/build.gradle.kts (1 hunks)
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt (2 hunks)
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1 hunks)
  • core/ui/src/main/res/values/strings.xml (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (7 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (5 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt (3 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (5 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (2 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (3 hunks)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt (2 hunks)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt (2 hunks)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt (1 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (3 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt (2 hunks)
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt (1 hunks)
  • feature/library/src/main/res/values/strings.xml (0 hunks)
  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt (2 hunks)
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt (3 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt (3 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt (2 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt (1 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt (2 hunks)
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt (1 hunks)
  • feature/search/src/main/res/values/strings.xml (0 hunks)
💤 Files with no reviewable changes (2)
  • feature/library/src/main/res/values/strings.xml
  • feature/search/src/main/res/values/strings.xml
🧰 Additional context used
🧠 Learnings (18)
📓 Common learnings
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#75
File: feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt:0-0
Timestamp: 2025-07-29T06:07:11.727Z
Learning: seoyoon513 팀에서는 UI 구현 단계에서 더미 데이터를 하드코딩하여 화면을 먼저 구현하고, 이후 서버 연동 시점에 실제 데이터로 교체하는 개발 방식을 사용합니다.
📚 Learning: 2025-07-28T18:22:00.618Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#72
File: feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/OcrPresenter.kt:70-70
Timestamp: 2025-07-28T18:22:00.618Z
Learning: LiveTextAnalyzer는 ML Kit 에러를 addOnFailureListener로 처리하지만 로그만 남기고 presenter에 실패를 알리지 않아서, UI에서 텍스트 인식 실패 상태를 표시할 수 없는 문제가 있음. StillTextAnalyzer처럼 실패 콜백을 추가하는 것이 더 나은 해결책임

Applied to files:

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt
📚 Learning: 2025-07-31T16:58:59.404Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt:11-18
Timestamp: 2025-07-31T16:58:59.404Z
Learning: Reed-Android 프로젝트는 현재 다국어 지원 계획이 없어서 모델에 한글 문자열을 직접 포함하는 것이 허용된다.

Applied to files:

  • core/ui/src/main/res/values/strings.xml
📚 Learning: 2025-07-31T16:58:59.404Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: core/model/src/main/kotlin/com/ninecraft/booket/core/model/EmotionModel.kt:11-18
Timestamp: 2025-07-31T16:58:59.404Z
Learning: Reed-Android 프로젝트에서 core:model 모듈은 순수 Kotlin 모듈이므로 Android 리소스(R.string 등)에 접근할 수 없다.

Applied to files:

  • core/ui/src/main/res/values/strings.xml
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
📚 Learning: 2025-07-31T23:17:40.054Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt:29-37
Timestamp: 2025-07-31T23:17:40.054Z
Learning: Reed-Android 프로젝트에서는 API가 준비되지 않은 상황에서 UI를 먼저 구현하고, API 연동 시점에 하드코딩된 데이터를 실제 데이터로 교체하는 개발 방식을 사용한다. RecordItem 컴포넌트의 emotionTags 매개변수도 API 연동 시점에 `text = emotionTags.joinToString(separator = "·") { "#$it" }`로 적용될 예정이다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
📚 Learning: 2025-07-20T12:34:23.786Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#61
File: feature/webview/build.gradle.kts:17-21
Timestamp: 2025-07-20T12:34:23.786Z
Learning: Reed-Android 프로젝트에서는 `booket.android.feature` convention plugin을 사용하여 feature 모듈들의 공통 의존성을 관리한다. 이 plugin은 Circuit, Compose, 그리고 core 모듈들의 의존성을 자동으로 포함하므로, 각 feature 모듈의 build.gradle.kts에서는 특별한 의존성(예: libs.logger, libs.kakao.auth)만 별도로 선언하면 된다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • core/ui/build.gradle.kts
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-31T23:22:02.816Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt:25-59
Timestamp: 2025-07-31T23:22:02.816Z
Learning: BookDetailScreen의 RecordsCollection 컴포넌트는 상위 Column의 verticalScroll과 LazyColumn의 무한 스크롤을 동시에 지원해야 하는 중첩 스크롤 시나리오입니다. LazyColumn에 고정 높이를 설정하지 않으면 앱이 충돌하는 이슈가 있어서, 현재는 높이를 계산하여 설정하고 userScrollEnabled = false로 구현되어 있습니다. 향후 InfiniteLazyColumn과 nestedScrollConnection을 도입하여 전체 화면 스크롤 + LazyColumn 무한 스크롤을 지원할 예정입니다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
📚 Learning: 2025-07-09T01:14:29.836Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#35
File: feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/TermsAgreementScreen.kt:127-127
Timestamp: 2025-07-09T01:14:29.836Z
Learning: In the Reed-Android project's TermsAgreementScreen.kt, the OnTermDetailClick event is intentionally passed an empty string for the URL parameter because the actual URLs for terms detail pages haven't been decided yet. This is a temporary implementation that will be updated once the URLs are finalized.

Applied to files:

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt
  • core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
📚 Learning: 2025-07-28T18:08:47.298Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#72
File: feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/CameraFrame.kt:21-82
Timestamp: 2025-07-28T18:08:47.298Z
Learning: Jetpack Compose에서 scale() 변환은 시각적 변환만 적용하며 레이아웃 좌표계는 변경하지 않는다. 따라서 scale(scaleX = -1f, scaleY = -1f)로 반전된 아이콘에서 padding()은 원래 레이아웃 기준으로 동작하므로, 시각적으로 올바른 위치를 위해서는 변환 전 좌표계 기준으로 padding을 설정해야 한다.

Applied to files:

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt
📚 Learning: 2025-07-12T01:33:57.101Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#45
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/appbar/ReedTopAppBar.kt:65-65
Timestamp: 2025-07-12T01:33:57.101Z
Learning: Reed Android 프로젝트에서 타이포그래피 사용 규칙: 톱 앱바(Top App Bar)에서는 `headline2SemiBold`를 사용하고, 바텀시트(Bottom Sheet)에서는 `heading2SemiBold`를 사용한다. 이는 의도적인 디자인 시스템 차별화이다.

Applied to files:

  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
📚 Learning: 2025-07-29T07:02:18.885Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#77
File: feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt:73-73
Timestamp: 2025-07-29T07:02:18.885Z
Learning: Kotlin에서 같은 패키지 내의 파일들은 패키지 레벨에 정의된 const val 상수를 import 없이 직접 접근할 수 있습니다. OnboardingPresenter.kt와 OnboardingUi.kt 모두 com.ninecraft.booket.feature.onboarding 패키지에 속해 있어서 ONBOARDING_STEPS_COUNT 상수를 공유할 수 있습니다.

Applied to files:

  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-16T15:54:19.322Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T15:54:19.322Z
Learning: In the Reed-Android project using Circuit architecture, DelegatingNavigator was restored because NavigableCircuitContent can only inject a single navigator, but the dual-navigator architecture (childNavigator for bottom navigation screens, rootNavigator for full-screen screens) requires a delegating component to work within Circuit's constraint.

Applied to files:

  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-31T23:30:37.547Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt:18-47
Timestamp: 2025-07-31T23:30:37.547Z
Learning: In Circuit architecture, presenters receive the Screen object directly as a constructor parameter (e.g., Assisted private val screen: RecordDetailScreen), and screen parameters are accessed through this screen object (e.g., screen.recordId). Screen parameters should not be added as separate constructor parameters.

Applied to files:

  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
  • feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-16T16:11:27.044Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegateNavigator.kt:0-0
Timestamp: 2025-07-16T16:11:27.044Z
Learning: In the Reed-Android project's DelegateNavigator implementation, the pop(), peek(), and peekBackStack() methods should always use childNavigator without branching logic, as they operate on the currently active navigation stack. Only goTo() and resetRoot() methods need to route between childNavigator and rootNavigator based on screen type.

Applied to files:

  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
📚 Learning: 2025-07-16T16:11:27.044Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegateNavigator.kt:0-0
Timestamp: 2025-07-16T16:11:27.044Z
Learning: In the Reed-Android project's DelegateNavigator implementation, the pop(), peek(), and peekBackStack() methods should always use childNavigator without branching logic, as they operate on the currently active navigation stack. Only goTo() and resetRoot() methods need to route between childNavigator and rootNavigator based on screen type using isMainTabScreen() function.

Applied to files:

  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
📚 Learning: 2025-07-08T12:33:01.863Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#32
File: core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt:10-16
Timestamp: 2025-07-08T12:33:01.863Z
Learning: Reed Android 프로젝트에서 KAKAO 버튼 스타일은 디자이너가 pressed 상태 색상을 별도로 정의하지 않았기 때문에 pressed 상태에서도 동일한 Kakao 색상을 사용한다.

Applied to files:

  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt
📚 Learning: 2025-07-22T05:19:10.071Z
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#63
File: feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt:28-53
Timestamp: 2025-07-22T05:19:10.071Z
Learning: feature/library의 LibraryPresenter에서 현재 FilterChipState의 카운트 값들은 UI 확인용 더미 데이터이며, API가 준비되면 실제 데이터로 교체될 예정입니다.

Applied to files:

  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
📚 Learning: 2025-07-14T00:51:38.952Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#46
File: core/network/src/main/kotlin/com/ninecraft/booket/core/network/response/BookSearchResponse.kt:33-44
Timestamp: 2025-07-14T00:51:38.952Z
Learning: The project uses Kotlinx Serialization with JSON configuration that includes `ignoreUnknownKeys = true`, `encodeDefaults = true`, `isLenient = true`, and `prettyPrint = true` to handle API response parsing robustly, making additional null safety measures less critical if the API consistently provides all expected fields.

Applied to files:

  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt
🧬 Code Graph Analysis (11)
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (1)
  • postErrorDialog (46-58)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordItem.kt (1)
  • getEmotionImageResourceByDisplayName (92-100)
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt (1)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
  • ReedErrorUi (22-48)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt (1)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
  • ReedErrorUi (22-48)
feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt (1)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt (1)
  • ReedDialog (25-111)
feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt (1)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
  • ReedErrorUi (22-48)
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (2)
core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/BookRepository.kt (1)
  • filterLibraryBooks (29-33)
core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultBookRepository.kt (1)
  • filterLibraryBooks (45-47)
feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (1)
  • postErrorDialog (46-58)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ReedButton.kt (1)
  • ReedButton (34-114)
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt (1)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
  • ReedErrorUi (22-48)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (5)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/NetworkImage.kt (1)
  • NetworkImage (17-39)
core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/ReedDivider.kt (1)
  • ReedDivider (13-21)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/QuoteBox.kt (1)
  • QuoteBox (16-49)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt (1)
  • ReviewBox (26-78)
core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)
  • ReedErrorUi (22-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (29)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/ErrorScope.kt (1)

3-5: 스코프 정의 방향 좋습니다

에러 범주가 명확해져 전역/국소 처리 분리가 쉬워집니다.

core/ui/src/main/res/values/strings.xml (1)

5-6: 에러 메시지 리소스 추가 적절합니다

네트워크/서버 에러를 구분한 2줄 메시지 구성은 UI 재사용에 유용합니다.

core/ui/build.gradle.kts (1)

15-15: core.ui → core.common 의존성 추가 검증 완료 (순환 의존 없음 확인)
의존성 추가 방향이 타당합니다. 제공해주신 스크립트로 확인한 결과 core.common에서 core.ui를 참조하는 부분이 없어 순환 의존이 없습니다.

feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt (1)

16-16: UiState.Error에 Throwable 보존: 정보 전달력 향상

예외를 그대로 전달해 에러 UI에서 맥락별 처리(네트워크/서버 등)가 쉬워집니다. 다만 화면에 exception.message를 직접 노출하지 않도록 유지 부탁드립니다(ReedErrorUi로 매핑).

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt (1)

17-17: 라이브러리도 Throwable 보존으로 통일: 일관성 확보

홈/검색/상세와 동일한 패턴으로 통일된 점 👍. 에러 UI 재사용성이 높습니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUiState.kt (1)

19-20: UiState.Error에 Throwable 전달 일관성 확인 완료

전체 코드베이스에서 UiState.Error(...) 호출부를 점검한 결과, 모두 Throwable(exception, e 등)을 인자로 넘기고 있으며 과거에 문자열(message)을 직접 넘기던 호출은 없습니다. 마이그레이션 누락 사항이 없으므로 추가 조치가 필요 없습니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUiState.kt (1)

16-17: 검증 완료: 호출부 마이그레이션 일관성 확인됨

라이브러리 검색과 도서 검색 모두
UiState.Error(exception) 형태로 예외 기반 에러 상태가 통일되어 있으며,
관련 호출부 마이그레이션이 누락 없이 적용되었습니다.

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt:85
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt:95
feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt (1)

170-175: ReedErrorUi 도입으로 에러 UI 일원화 완료

  • state.uiState.exception을 전달하여 네트워크/서버 에러 메시지 분기 및 재시도 콜백을 통합 처리한 점 좋습니다.
  • 하단 탭바(MainBottomBar) 위 영역만 에러 UI로 채워져 UX 일관성 유지가 됩니다.

(확인 사항) Presenter의 OnRetryClick가 초기 홈 데이터 재로딩으로 정확히 이어지는지 한번 더 점검 부탁드립니다.

Also applies to: 30-30

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt (2)

12-15: 전역 에러 다이얼로그 도입을 위한 import 추가, 방향성 적합합니다.

새 전역 에러 처리 플로우(ErrorScope + postErrorDialog) 적용을 위한 준비로 보이며 일관성 있는 설계입니다.


95-95: 초기 로딩 실패 시 예외 객체 전달로 전환한 점 LGTM

UiState.Error에 Throwable을 담아 ReedErrorUi에서 타입 기반 메시지 분기가 가능해졌습니다. 다른 모듈과의 일관성도 확보됩니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchUi.kt (2)

29-29: 공통 에러 UI 컴포넌트 도입 import, 잘 적용되었습니다.

모듈 전반에서 에러 UI를 통일하는 방향과 일치합니다.


107-110: ReedErrorUi로 에러 UI 일원화, 적합합니다.

예외를 그대로 전달해 네트워크/서버 에러 메시지를 표준화하고, Retry 이벤트 연결도 명확합니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt (1)

85-85: 초기 페이지 실패 시 예외 객체 전달로 일관성 확보, LGTM

풀스크린 에러 UI(ReedErrorUi)와 맞물려 일관된 에러 표기가 가능합니다. 페이징 실패 시 FooterState.Error는 문자열 유지로 충분합니다.

feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt (1)

52-52: 예외 객체 기반 에러 상태 전환, 일관성 좋습니다

ReedErrorUi와의 결합을 고려한 합리적 변경입니다.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUi.kt (2)

26-26: 공통 에러 UI 컴포넌트 import, 적절합니다

모듈 간 에러 UI 일관성 확보에 기여합니다.


150-153: ReedErrorUi 적용으로 에러 UI 일관화, 잘 반영되었습니다

예외를 그대로 전달하여 메시지 표준화 및 재시도 핸들링이 단순해졌습니다.

feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt (2)

116-123: 중복 호출 방지 가드 👍

현재 필터와 동일한 옵션일 때 조기 return 을 추가해 불필요한 API 호출을 막았습니다. 성능·트래픽 모두 이득입니다.


154-160: 초기 로드 호출 위치 OK

LaunchedEffect(Unit) 내에서 조건 없이 초기 데이터를 불러오도록 단순화한 변경은 논리적으로 문제없어 보입니다.
재합성에도 재호출되지 않으니(side-effect key가 Unit) 안심하셔도 됩니다.

feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt (1)

120-129: 다이얼로그·토스트 이중 알림 여부 확인 요청

postErrorDialog로 전역 다이얼로그를 띄운 뒤, 곧바로 handleException 내에서 토스트도 표시합니다.
동일 오류에 대해 다이얼로그 + 토스트가 연달아 노출되면 사용자 경험이 과도하게 시끄러울 수 있습니다.
두 가지 알림이 모두 필요한지 한 번 더 점검해주세요.

core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedDialog.kt (1)

30-30: title nullable 처리 👍

타이틀을 선택적으로 받도록 변경해 다이얼로그 재사용성이 좋아졌습니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt (2)

20-21: 예외 객체 전달로 정보 보존 👍

에러 상태가 메시지 문자열 대신 Throwable을 보유하도록 변경되어 스택트레이스 등 상세 정보를 활용할 수 있게 됐습니다.


66-67: 재시도 이벤트 추가 확인

OnRetryClick 이벤트가 정의되었습니다. 프레젠터에서 해당 이벤트를 처리하고 있는지, UI에서 적절히 호출되는지 확인만 부탁드립니다.

feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchUi.kt (2)

34-34: ReedErrorUi 도입으로 에러 UI 일관화 👍

공통 에러 컴포넌트로의 전환이 적절합니다. 모듈 간 일관성 측면에서 좋습니다.


127-130: 에러 상태 렌더링을 ReedErrorUi로 교체 — 올바른 사용

Throwable 기반의 예외 전달과 재시도 콜백 연결이 명확합니다. 화면 최초 로딩 실패 시 풀스크린 에러로의 전환 정책에도 부합합니다.

core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedErrorUi.kt (1)

22-48: 컴포넌트 구현 전반 LGTM

네트워크/서버 예외 분기와 공통 테마 적용, 재시도 버튼 연결까지 목적에 부합합니다. 미리보기까지 포함되어 있어 디자인 확인도 수월합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (2)

9-14: UiState 정의 방향 적절

Idle/Loading/Success/Error(예외 포함) 구성이 명확합니다. 에러 UI 및 재시도 처리와 잘 맞습니다.


17-17: RecordDetailUiState에 uiState 추가 LGTM

기존 상태와 충돌 없고, 기본값 Idle 설정도 적절합니다.

feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (2)

153-156: loadMore 중복 호출 가드 확인 요청

InfinityLazyColumn에서 스크롤 바닥 진입 시 OnLoadMore가 빠르게 연속 호출될 수 있습니다. 컴포넌트 내부에서 디바운스/스로틀이 없거나, footerState 로딩 중 재호출을 막지 않는다면 presenter 레벨에서 in-flight 플래그로 중복 요청을 방지해 주세요.


285-290: ReedErrorUi 연동 및 재시도 콜백 전달 훌륭합니다

예외 기반 에러 모델과 재시도 이벤트가 잘 연결되어 있습니다. 전역/로컬 에러 UX 방향과도 일치합니다.

Comment on lines +50 to 56
Image(
painter = painterResource(getEmotionImageResourceByDisplayName(emotion)),
contentDescription = "Emotion Graphic",
modifier = Modifier
.size(ReedTheme.spacing.spacing10)
.background(
color = ReedTheme.colors.bgTertiary,
shape = CircleShape,
)
.clip(shape = CircleShape),
.clip(CircleShape),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

감정 이미지 표시 개선은 적절하나, 접근성(i18n) 및 안전성 보완 필요

  • contentDescription가 하드코딩되어 있어 현지화가 어렵습니다. stringResource로 교체해 주세요.
  • getEmotionImageResourceByDisplayName(emotion)는 표시 문자열 의존이라 로케일/카피 변경에 취약합니다. 가능하면 안정 키(코드/enum) 기반 매핑으로 전환을 권장합니다.

예시:

- Image(
-   painter = painterResource(getEmotionImageResourceByDisplayName(emotion)),
-   contentDescription = "Emotion Graphic",
-   modifier = Modifier
-     .size(ReedTheme.spacing.spacing10)
-     .clip(CircleShape),
- )
+ Image(
+   painter = painterResource(getEmotionImageResourceByDisplayName(emotion)),
+   contentDescription = stringResource(R.string.cd_emotion_graphic),
+   modifier = Modifier
+     .size(ReedTheme.spacing.spacing10)
+     .clip(CircleShape),
+ )
🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/component/ReviewBox.kt
around lines 50 to 56, replace the hardcoded contentDescription string with a
localized string using stringResource for accessibility and i18n support.
Additionally, refactor the image resource retrieval to use a stable key such as
an enum or code instead of relying on display name strings, to improve
robustness against locale or copy changes.

Comment on lines 38 to 49
fun getRecordDetail(readingRecordId: String) {
scope.launch {
uiState = UiState.Loading

repository.getRecordDetail(readingRecordId = readingRecordId)
.onSuccess { result ->
uiState = UiState.Success
recordDetailInfo = result
}
.onFailure { exception ->
uiState = UiState.Error(exception)
val handleErrorMessage = { message: String ->
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

중복 요청/경쟁 상태 가능성 — 이전 fetch 취소 로직 추가 권장

연속 재시도 시 이전 Job이 뒤늦게 완료되면 최신 결과를 덮어쓸 수 있습니다. 이전 작업 취소 후 새 작업을 시작하세요.

-        fun getRecordDetail(readingRecordId: String) {
-            scope.launch {
+        fun getRecordDetail(readingRecordId: String) {
+            // 이전 요청 취소 후 새 요청 시작
+            fetchJob?.cancel()
+            fetchJob = scope.launch {
                 uiState = UiState.Loading
                 repository.getRecordDetail(readingRecordId = readingRecordId)
                     .onSuccess { result ->
                         uiState = UiState.Success
                         recordDetailInfo = result
                     }
                     .onFailure { exception ->
                         uiState = UiState.Error(exception)
                         val handleErrorMessage = { message: String ->
                             Logger.e(message)
                             sideEffect = RecordDetailSideEffect.ShowToast(message)
                         }

위 변경에 필요한 보조 코드(파일 상단부에 추가):

// 추가 import
import kotlinx.coroutines.Job

// present() 내부 상태 정의 근처
var fetchJob by rememberRetained { mutableStateOf<Job?>(null) }
🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
around lines 38 to 49, the getRecordDetail function launches a coroutine without
cancelling any previous ongoing fetch, which can cause outdated results to
overwrite newer ones. To fix this, declare a Job variable to keep track of the
current fetch coroutine, cancel the existing fetchJob if it is active before
launching a new coroutine, and assign the new coroutine's Job to fetchJob. Also,
add the necessary import for kotlinx.coroutines.Job and define the fetchJob
state variable near the present() function's state declarations as shown in the
comment.

Comment thread feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt Outdated
data object Loading : UiState
data object Success : UiState
data object Error : UiState
data class Error(val exception: Throwable) : UiState
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

예외 메시지 직접 노출 여부 점검 스크립트

혹시 남아있는 raw message 노출이 있는지 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# exception.message 직접 노출 탐지
rg -n "exception\\.message|Throwable\\.message"

Length of output: 2366


사용자에게 예외 메시지(raw message) 직접 노출 지점 확인 및 처리 필요
스크립트 실행 결과, 아래 파일들에서 exception.message 또는 Throwable.message 를 직접 호출하여 UI나 로그에 노출하고 있습니다. 사용자에게는 보다 친화적인 메시지를 보여주어야 하므로, 중앙화된 예외 처리 로직(core/common/utils/HandleException.kt)을 활용하거나 커스텀 메시지 변환 함수를 만들어 아래 위치를 우선 수정해 주세요.

  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/library/LibrarySearchPresenter.kt:83
  • feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt:93
  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt:92
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt:173
  • core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/StillTextAnalyzer.kt:58
  • core/ocr/src/main/kotlin/com/ninecraft/booket/core/ocr/analyzer/LiveTextAnalyzer.kt:55, 64
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt:46, 47, 74, 75
  • feature/login/src/main/kotlin/com/ninecraft/booket/feature/termsagreement/TermsAgreementPresenter.kt:75, 76
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt:39

이 부분들을 exception.message ?: defaultMessage 형태가 아닌, HandleException.getUserFriendlyMessage(exception) 등으로 대체하여 예외 메시지 노출을 방지해 주세요.

🤖 Prompt for AI Agents
In feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUiState.kt
at line 16, the Error data class exposes the raw exception message directly. To
prevent showing raw exception messages to users, replace any direct usage of
exception.message with a call to a centralized exception handler like
HandleException.getUserFriendlyMessage(exception) or a custom message conversion
function. This ensures user-friendly error messages are displayed consistently
across the app.

data object Loading : UiState
data object Success : UiState
data class Error(val message: String) : UiState
data class Error(val exception: Throwable) : UiState
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

예외 메시지 직접 노출 점검

라이브러리 화면에서도 raw exception.message 노출이 없는지 확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
# Library 모듈 내 message 노출 탐지
rg -n "exception\\.message|Throwable\\.message" feature/library

Length of output: 240


예외 메시지 직접 노출 방지 필요

UI에 exception.message를 그대로 노출하고 있어 민감 정보 유출 및 보안‧사용자 경험 이슈가 발생할 수 있습니다. 사용자에게는 내부 예외 메시지 대신 친숙한 오류 메시지를 제공하도록 수정해주세요.

  • feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt:92
    현재 코드:
    val errorMessage = exception.message ?: "알 수 없는 오류가 발생했습니다."
    exception.message 대신 공통 에러 핸들러나 커스텀 매핑 함수(e.g. getUserFriendlyError(exception))를 사용해 사용자 친화적인 메시지를 리턴하도록 변경 바랍니다.
🤖 Prompt for AI Agents
In
feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt
at line 17, the Error data class exposes the raw exception which leads to
displaying sensitive exception messages directly in the UI. To fix this, avoid
using exception.message directly for user-facing messages. Instead, implement or
use a common error handler or a custom mapping function like
getUserFriendlyError(exception) to convert exceptions into user-friendly error
messages before passing them to the UI.

Comment on lines +120 to +123
postErrorDialog(
errorScope = ErrorScope.BOOK_REGISTER,
exception = exception,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

에러 다이얼로그에 재시도 action을 연결하세요 + 중복 피드백(다이얼로그+토스트) 방지 권장

현재 다이얼로그는 action 없이 표시됩니다. 실패 시 재시도 버튼이 있는 사양이라면 기존 파라미터로 재시도 가능하도록 action을 전달해 UX를 완성하는 것을 권장합니다. 또한 바로 아래에서 토스트/handleException으로 별도 피드백을 중복 노출하고 있어 사용자 경험이 지저분해질 수 있습니다. BOOK_REGISTER 스코프는 다이얼로그 우선으로 정리하는 편이 명확합니다.

다이얼로그에 재시도 action 연결(이 변경 범위 내에서 적용 가능):

 postErrorDialog(
     errorScope = ErrorScope.BOOK_REGISTER,
     exception = exception,
+    action = { upsertBook(isbn13, bookStatus) },
 )

다이얼로그와 토스트의 중복을 피하려면, 아래 블록을 상황에 따라 제거하거나(권장) 네트워크 외 에러에만 토스트를 보이도록 분기하세요(선택):

// 옵션 A(권장): BOOK_REGISTER 실패는 다이얼로그만 사용
// handleException(...) 호출 제거

// 옵션 B(선택): 네트워크 외 에러에만 토스트
if (!exception.isNetworkError()) {
    handleException(
        exception = exception,
        onError = handleErrorMessage,
        onLoginRequired = { navigator.resetRoot(LoginScreen) },
    )
}
🤖 Prompt for AI Agents
In
feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/book/BookSearchPresenter.kt
around lines 120 to 123, the postErrorDialog call lacks a retry action, and
there is redundant user feedback due to both dialog and toast being shown. Fix
this by adding a retry action parameter to postErrorDialog to enable retry
functionality. Then, remove the handleException call below to avoid duplicate
feedback, or alternatively, conditionally show the toast only for non-network
errors by wrapping handleException in a network error check.

@easyhooon
Copy link
Copy Markdown
Contributor

easyhooon commented Aug 8, 2025

네트워크 없는 환경에서 Splash 진입 시 바로 로그인 화면으로 이동하는 이슈 있습니다. Splash에서 재시도 다이얼로그를 띄우던지 해야할 것 같네요

헤이딜러나 Soop과 같은 앱의 경우엔 이 경우 스플래시 화면에서 다이얼로그로 안내 후 앱을 종료하는 전략을 세우고 있더군여. 아무래도 앱을 시작할때 저희는 아직 없지만 서버에 사용자 정보를 보내는 경우가 있는데(fcm 토큰을 보낸다던지), 이거 부터 실패하면 이후 앱을 사용하는데 문제가 있으므로 앱을 종료시키더군여. 저도 이 플로우에 대해선 어느정도 동의하는 부분입니다.

@easyhooon
Copy link
Copy Markdown
Contributor

특히 로그인의 경우 카카오톡로그인, Reed회원가입/로그인 으로 구분되어 있어 이 부분 에러 처리도 어떻게 해야할지 고민이 필요합니다

이 부분은 토스트만으로 해도 될 것 같은...

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (1)

97-101: 첫 페이지 로드 후 isLastPage·currentStartIndex 갱신 누락
초기 로드가 완료돼도 두 변수가 초기값(0/false)에 머물러 PAGE_SIZE 미만 데이터일 때 추가 로드를 계속 시도합니다.
지난 리뷰에서도 동일 지적이 있었으므로 반드시 반영해 주세요.

bookDetail = detail
seedsStates = seeds.categories.toImmutableList()
readingRecords = records.content.toPersistentList()

currentStartIndex = START_INDEX
isLastPage = records.content.size < PAGE_SIZE
🧹 Nitpick comments (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (2)

135-144: 로딩 인디케이터 접근성 개선 필요

CircularProgressIndicatorcontentDescription이 없어 스크린리더 사용 시 로딩 상태를 인지할 수 없습니다.
Modifier.semantics { contentDescription = ... } 또는 progressSemantics()를 적용해 주세요.


284-289: Error UI 크기 지정 누락

ReedErrorUiModifier.fillMaxSize() 없이 배치되면 상위 레이아웃에 따라 중앙 정렬이 깨질 수 있습니다.
modifier = Modifier.fillMaxSize()를 넘겨 전체 화면을 일관되게 덮도록 하는 편이 안전합니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7136609 and 5debfeb.

📒 Files selected for processing (7)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt (1 hunks)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (3 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (7 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt (4 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt (5 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt (2 hunks)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt
🚧 Files skipped from review as they are similar to previous changes (4)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUiState.kt
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailUi.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#75
File: feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt:0-0
Timestamp: 2025-07-29T06:07:11.727Z
Learning: seoyoon513 팀에서는 UI 구현 단계에서 더미 데이터를 하드코딩하여 화면을 먼저 구현하고, 이후 서버 연동 시점에 실제 데이터로 교체하는 개발 방식을 사용합니다.
📚 Learning: 2025-07-20T12:34:23.786Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#61
File: feature/webview/build.gradle.kts:17-21
Timestamp: 2025-07-20T12:34:23.786Z
Learning: Reed-Android 프로젝트에서는 `booket.android.feature` convention plugin을 사용하여 feature 모듈들의 공통 의존성을 관리한다. 이 plugin은 Circuit, Compose, 그리고 core 모듈들의 의존성을 자동으로 포함하므로, 각 feature 모듈의 build.gradle.kts에서는 특별한 의존성(예: libs.logger, libs.kakao.auth)만 별도로 선언하면 된다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-31T23:22:02.816Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt:25-59
Timestamp: 2025-07-31T23:22:02.816Z
Learning: BookDetailScreen의 RecordsCollection 컴포넌트는 상위 Column의 verticalScroll과 LazyColumn의 무한 스크롤을 동시에 지원해야 하는 중첩 스크롤 시나리오입니다. LazyColumn에 고정 높이를 설정하지 않으면 앱이 충돌하는 이슈가 있어서, 현재는 높이를 계산하여 설정하고 userScrollEnabled = false로 구현되어 있습니다. 향후 InfiniteLazyColumn과 nestedScrollConnection을 도입하여 전체 화면 스크롤 + LazyColumn 무한 스크롤을 지원할 예정입니다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
📚 Learning: 2025-07-29T07:02:18.885Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#77
File: feature/onboarding/src/main/kotlin/com/ninecraft/booket/feature/onboarding/OnboardingUi.kt:73-73
Timestamp: 2025-07-29T07:02:18.885Z
Learning: Kotlin에서 같은 패키지 내의 파일들은 패키지 레벨에 정의된 const val 상수를 import 없이 직접 접근할 수 있습니다. OnboardingPresenter.kt와 OnboardingUi.kt 모두 com.ninecraft.booket.feature.onboarding 패키지에 속해 있어서 ONBOARDING_STEPS_COUNT 상수를 공유할 수 있습니다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
📚 Learning: 2025-07-16T15:54:19.322Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#52
File: feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/screens/DelegatingNavigator.kt:0-0
Timestamp: 2025-07-16T15:54:19.322Z
Learning: In the Reed-Android project using Circuit architecture, DelegatingNavigator was restored because NavigableCircuitContent can only inject a single navigator, but the dual-navigator architecture (childNavigator for bottom navigation screens, rootNavigator for full-screen screens) requires a delegating component to work within Circuit's constraint.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUi.kt
🧬 Code Graph Analysis (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (2)
  • handleException (17-44)
  • postErrorDialog (46-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build

Comment on lines +238 to 242
is BookDetailUiEvent.OnRetryClick -> {
scope.launch {
initialLoad()
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Retry 시 페이징 상태 초기화 필요

OnRetryClick에서 initialLoad()만 호출하면 isLastPage, currentStartIndex, footerState 등이 이전 값 그대로 남습니다.
재시도 전에 해당 값을 초기화하지 않으면 잘못된 페이징 동작과 UI 상태가 발생할 수 있습니다.

🤖 Prompt for AI Agents
In
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
around lines 238 to 242, the retry handler calls initialLoad() without resetting
paging state variables like isLastPage, currentStartIndex, and footerState. To
fix this, add code to reset these paging-related variables to their initial
values before calling initialLoad() in the OnRetryClick event handler to ensure
correct paging behavior and UI state on retry.

@seoyoon513
Copy link
Copy Markdown
Contributor Author

현재 에러 관련 토스트로 처리한 부분은 굳이 제거하지 않았습니다. 기능 QA이후 결정이 되면 그때 다듬어요!

여기 다이얼로그 있으면 굳이 토스트 안보여줘도될것같긴하네여

현재 에러 다이얼로그 플로우는 아래와 같은데요,

  • 확인 -> 다이얼로그 닫힘 -> 사용자 자율로 재시도 or 화면 탐색

토스트나 스낵바로도 충분하지 않을까..라는 생각도 있습니다

@easyhooon
Copy link
Copy Markdown
Contributor

토스트나 스낵바로도 충분하지 않을까..라는 생각도 있습니다

맞아여 버튼은 다시 누를수있으니까여

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (1)

241-244: 재시도 시 페이징 상태 초기화 누락 — 이전 리뷰와 동일

initialLoad() 호출 전에 footerState(특히 Error 상태) 를 Idle 로,
readingRecords, isLastPage, currentStartIndex 등을 초기화하지 않으면
재시도 후에도 잘못된 페이징 UI 가 남을 수 있습니다.

이 부분은 이전 커밋에서도 지적되었으므로 참고 바랍니다.

🧹 Nitpick comments (2)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (2)

82-95: 동시 요청 실패 시 다른 요청까지 취소될 수 있습니다

coroutineScope { async { … } } 구조에서는 하나의 네트워크 호출이 예외를 던지면 나머지 async 작업도 전파된 취소로 중단됩니다.
각 요청을 독립적으로 시도하고, 일부만 실패해도 전체 UI 를 에러로 전환할지 선택하려면 supervisorScope 로 감싸는 편이 안전합니다.

supervisorScope {
    val detailDef = async { … }
    val seedsDef  = async { … }
    val recordsDef = async { … }
    // 필요에 따라 개별 await + 개별 에러 처리
}

이렇게 하면 특정 호출 실패 시 다른 호출이 불필요하게 취소되는 것을 방지할 수 있습니다.


135-139: 다이얼로그와 토스트가 중복으로 표시됩니다

postErrorDialog() 로 오류 다이얼로그를 띄운 뒤 바로 아래에서 동일 오류를 ShowToast 로도 알리고 있습니다.
사용자에게 동일 메시지가 두 번 노출되어 UX 가 과도할 수 있으니 하나만 남기고 통일하는 것을 권장드립니다.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5debfeb and 5f94120.

📒 Files selected for processing (5)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt (1 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (7 hunks)
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt (3 hunks)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt (2 hunks)
  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • core/common/src/main/kotlin/com/ninecraft/booket/core/common/event/ErrorEventHelper.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
  • feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/record/RecordDetailPresenter.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#46
File: feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/component/InfiniteLazyColumn.kt:83-95
Timestamp: 2025-07-14T00:46:03.843Z
Learning: seoyoon513과 팀은 한국어 주석을 선호하며, 한국어 주석을 영어로 번역하라는 제안을 하지 않아야 함
Learnt from: seoyoon513
PR: YAPP-Github/Reed-Android#75
File: feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeUi.kt:0-0
Timestamp: 2025-07-29T06:07:11.727Z
Learning: seoyoon513 팀에서는 UI 구현 단계에서 더미 데이터를 하드코딩하여 화면을 먼저 구현하고, 이후 서버 연동 시점에 실제 데이터로 교체하는 개발 방식을 사용합니다.
📚 Learning: 2025-07-31T23:22:02.816Z
Learnt from: easyhooon
PR: YAPP-Github/Reed-Android#88
File: feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/component/RecordsCollection.kt:25-59
Timestamp: 2025-07-31T23:22:02.816Z
Learning: BookDetailScreen의 RecordsCollection 컴포넌트는 상위 Column의 verticalScroll과 LazyColumn의 무한 스크롤을 동시에 지원해야 하는 중첩 스크롤 시나리오입니다. LazyColumn에 고정 높이를 설정하지 않으면 앱이 충돌하는 이슈가 있어서, 현재는 높이를 계산하여 설정하고 userScrollEnabled = false로 구현되어 있습니다. 향후 InfiniteLazyColumn과 nestedScrollConnection을 도입하여 전체 화면 스크롤 + LazyColumn 무한 스크롤을 지원할 예정입니다.

Applied to files:

  • feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt
🧬 Code Graph Analysis (1)
feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailPresenter.kt (1)
core/common/src/main/kotlin/com/ninecraft/booket/core/common/utils/HandleException.kt (2)
  • handleException (17-44)
  • postErrorDialog (46-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build

},
)
try {
coroutineScope {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 정의한 rememberCoroutineScope 쓰면 되지않을까여

uiState = UiState.Success
}
} catch (ce: CancellationException) {
throw ce
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 ce를 presenter에서 던져도 CEH같은게 없다보니 던지는게 무의미하긴합니다.

)
try {
coroutineScope {
val bookDetailDef = async { bookRepository.getBookDetail(screen.isbn13).getOrThrow() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 Def가 Deferred 의 약자인가보군여

getSeedsStats()
getBookDetail()
getReadingRecords()
initialLoad()
Copy link
Copy Markdown
Contributor

@easyhooon easyhooon Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialLoad로 함수 빼내는건 좋다고 생각하는데, 그러면 위와 같이 한 함수내애 async { } 로 모두 처리해줘야하는지라 좀 복잡하다고 생각해서 전 기존에 각각의 함수를 정의하고 scope.launch { }의 방식으로 구현을 했던건데요.(각각이 별도의 launch {} 내부에서 실행되기 때문에 3개의 함수는 병렬로 실행됨)

LaunchedEffect의 key로 retry flag 추가한다면 정의한다면, retry 상황에서 해당 flag만 true로 바꿔서 LaunchedEffect 내부 블럭을 재실행(재시도)시킬수도 있었을것같아여, 전 이 방식이 조금 더 낫다고 생각함다

Copy link
Copy Markdown
Contributor

@easyhooon easyhooon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 QA 진행을 위해 Approve!

@seoyoon513 seoyoon513 merged commit 1a4ba6f into develop Aug 8, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BOOK-233/feat] 에러 핸들링 및 UI 처리

2 participants